package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
@RequiredArgsConstructor
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;
    private final ExecutorService executorService = Executors.newFixedThreadPool(10);

    /**
     * 将任意对象缓存到Redis
     */
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 将任意对象缓存到redis,并设置逻辑过期时间
     *
     * @param key        key
     * @param value      对象
     * @param expireTime 过期时间
     * @param timeUnit   时间单位
     */
    public void setWithLogicExpire(String key, Object value, Long expireTime, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plus(expireTime, ChronoUnit.valueOf(timeUnit.name())));
        redisData.setData(value);

        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 查询缓存,如果未查询到,通过查询数据库,并设置缓存过期时间
     *
     * @param prefix     key前缀
     * @param id         id
     * @param type       返回数据类型
     * @param dbRollback 获取缓存失败时,从数据库查询的函数
     * @param lockPrefix 锁前缀
     * @param expireTime 过期时间
     * @param timeUnit   时间单位
     * @param <R>        返回数据类型
     * @param <ID>       id的通用类型
     * @return
     */
    public <R, ID> R getCacheWithThrough(
            String prefix, ID id, Class<R> type, Function<ID, R> dbRollback, String lockPrefix, Long expireTime, TimeUnit timeUnit) {
        String key = prefix + id;
        // 从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            //转为指定对象,并返回
            return JSONUtil.toBean(json, type);
        }
        if (json == null) {
            // 缓存命中,返回
            return null;
        }
        //未查询到缓存,从数据库获取数据,写入缓存,并设置过期时间
        //获取锁
        String lockKey = lockPrefix + id;
        try {
            if (!tryLock(lockKey)) {
                // 未获取到锁
                Thread.sleep(50);
                // 重新获取数据
                return getCacheWithThrough(prefix, id, type, dbRollback, lockPrefix, expireTime, timeUnit);
            }
            // 获取到锁,更新缓存

            //再次检验缓存是否过期
            json = stringRedisTemplate.opsForValue().get(key);

            R r = dbRollback.apply(id);
            if (ObjectUtil.isNull(r)) {
                //如果未空,代表数据库不存在,缓存空值(防止缓存穿透)
                this.set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //写入缓存
            this.set(key, r, expireTime, timeUnit);
            return r;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(lockKey);
        }
    }

    /**
     * @param prefix     前缀
     * @param id         id
     * @param type       返回数据类型
     * @param dbRollback 数据库查询函数
     * @param lockPrefix 锁前缀
     * @param expireTime 逻辑过期时间
     * @param timeUnit   时间单位
     * @param <R>        返回数据类型
     * @param <ID>       id类型
     * @return
     */
    public <R, ID> R getCacheWithLogicExpire(
            String prefix, ID id, Class<R> type, Function<ID, R> dbRollback, String lockPrefix, Long expireTime, TimeUnit timeUnit) {
        String key = prefix + id;
        // 从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)) {
            return null;
        }
        // 查询到缓存,转为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);

        LocalDateTime expire = redisData.getExpireTime();
        //判断缓存是否过期
        if (expire.isAfter(LocalDateTime.now())) {
            // 未过期
            return r;
        }
        //已过期
        String lockKey = lockPrefix + id;
        if (tryLock(lockKey)) {
            //开启线程重构缓存
            executorService.submit(() -> {
                // 获取锁成功,再次获取缓存并校验是否过期
                String renewedJson = stringRedisTemplate.opsForValue().get(key);
                RedisData renewedData = JSONUtil.toBean(renewedJson, RedisData.class);
                if (renewedData.getExpireTime().isAfter(LocalDateTime.now())) {
                    return;
                }
                // 查询数据库
                R obj = dbRollback.apply(id);
                if (obj != null) {
                    setWithLogicExpire(key, obj, expireTime, timeUnit);
                }
            });
        }
        return r;
    }

    private boolean tryLock(String lockKey) {
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, Thread.currentThread().getName(), Duration.ofSeconds(10L));
        return BooleanUtil.isTrue(aBoolean);
    }

    private void unLock(String lockKey) {
        stringRedisTemplate.delete(lockKey);
    }
}
